/*-------------------------------------------------------------------------
 *
 * standby.c
 *	  在热备模式中使用的杂项函数。
 *
 * 处理与 RM_STANDBY_ID 相关的所有函数，这与 AccessExclusiveLocks 和启动热
 * 备模式的快照相关。加上冲突恢复处理。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/storage/ipc/standby.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"
#include "access/transam.h"
#include "access/twophase.h"
#include "access/xact.h"
#include "access/xloginsert.h"
#include "access/xlogrecovery.h"
#include "access/xlogutils.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "storage/bufmgr.h"
#include "storage/lmgr.h"
#include "storage/proc.h"
#include "storage/procarray.h"
#include "storage/sinvaladt.h"
#include "storage/standby.h"
#include "utils/hsearch.h"
#include "utils/memutils.h"
#include "utils/ps_status.h"
#include "utils/timeout.h"
#include "utils/timestamp.h"

/* 用户可设置的 GUC 参数 */
int			vacuum_defer_cleanup_age;
int			max_standby_archive_delay = 30 * 1000;
int			max_standby_streaming_delay = 30 * 1000;
bool		log_recovery_conflict_waits = false;

static HTAB *RecoveryLockLists;

/* 超时处理程序设置的标志 */
static volatile sig_atomic_t got_standby_deadlock_timeout = false;
static volatile sig_atomic_t got_standby_delay_timeout = false;
static volatile sig_atomic_t got_standby_lock_timeout = false;

static void fc_ResolveRecoveryConflictWithVirtualXIDs(VirtualTransactionId *fc_waitlist,
												   ProcSignalReason fc_reason,
												   uint32 fc_wait_event_info,
												   bool fc_report_waiting);
static void fc_SendRecoveryConflictWithBufferPin(ProcSignalReason fc_reason);
static XLogRecPtr fc_LogCurrentRunningXacts(RunningTransactions fc_CurrRunningXacts);
static void fc_LogAccessExclusiveLocks(int fc_nlocks, xl_standby_lock *fc_locks);
static const char *get_recovery_conflict_desc(ProcSignalReason fc_reason);

/*
 * 跟踪给定事务拥有的所有锁。
 */
typedef struct RecoveryLockListsEntry
{
	TransactionId xid;
	List	   *locks;
} RecoveryLockListsEntry;

/*
 * InitRecoveryTransactionEnvironment
 *		初始化我们主服务器正在进行的事务的跟踪。
 *
 * 我们需要发布共享失效并保持锁定。保持锁定
 * 意味着其他人可能会想要等待我们，因此我们需要创建一个锁表
 * vxact 条目就像一个真实的事务。我们可以为每个事务创建和删除
 * 锁表条目，但创建一个永久条目并始终保留在那里更简单。
 * 然后根据需要获取和释放锁定。是的，这意味着一旦我们运行这个，
 * 您可以在 pg_locks 中看到启动过程。
 */
void InitRecoveryTransactionEnvironment(void)
{
	VirtualTransactionId fc_vxid;
	HASHCTL		fc_hash_ctl;

	/*
	 * 初始化跟踪每个事务持有的锁的 hash 表。
	 */
	fc_hash_ctl.keysize = sizeof(TransactionId);
	fc_hash_ctl.entrysize = sizeof(RecoveryLockListsEntry);
	RecoveryLockLists = hash_create("RecoveryLockLists",
									64,
									&fc_hash_ctl,
									HASH_ELEM | HASH_BLOBS);

	/*
	 * 为启动过程初始化共享失效管理，
	 * 小心地将自己注册为 sendOnly 进程，这样我们无需读取消息，
	 * 也不会在队列开始填满时被通知。
	 */
	SharedInvalBackendInit(true);

	/*
	 * 为启动过程锁定一个虚拟事务 ID。
	 *
	 * 我们需要运行 GetNextLocalTransactionId() ，因为
	 * SharedInvalBackendInit() 使 localTransactionId 无效，而锁
	 * 管理器对此非常不喜。
	 *
	 * 请注意，我们不需要运行 XactLockTableInsert()，因为没有人
	 * 需要等待 xids。这听起来有点奇怪，但表锁由 vxids 保持，
	 * 行级锁由 xids 保持。所有查询均持有 AccessShareLocks，因此在我们
	 * 写入或锁定新行时永远不会阻塞。
	 */
	fc_vxid.backendId = MyBackendId;
	fc_vxid.localTransactionId = GetNextLocalTransactionId();
	VirtualXactLockTableInsert(fc_vxid);

	standbyState = STANDBY_INITIALIZED;
}

/*
 * ShutdownRecoveryTransactionEnvironment
 *		关闭事务跟踪
 *
 * 准备从热备用模式切换到正常操作。关闭
 * 恢复时间事务跟踪。
 *
 * 即使在启动过程中关闭时，如果事务跟踪已初始化，
 * 也必须调用此函数。否则一些被跟踪
 * 的事务持有的锁将不会被释放，并可能干扰
 * 在启动过程中即将退出的仍在运行的过程。
 */
void ShutdownRecoveryTransactionEnvironment(void)
{
	/*
	 * 如果 RecoveryLockLists 为 NULL，则不执行任何操作，
	 * 因为这意味着事务跟踪尚未初始化或已经关闭。
	 * 这可以防止事务跟踪意外关闭超过一次。
	 */
	if (RecoveryLockLists == NULL)
		return;

	/* 将所有跟踪的进行中事务标记为已完成。 */
	ExpireAllKnownAssignedTransactionIds();

	/* 释放被跟踪事务持有的所有锁 */
	StandbyReleaseAllLocks();

	/* 销毁锁的 hash 表。 */
	hash_destroy(RecoveryLockLists);
	RecoveryLockLists = NULL;

	/* 清理我们的 VirtualTransaction */
	VirtualXactLockTableCleanup();
}



/*
 * -----------------------------------------------------
 *		备用等待定时器和后端取消逻辑
 * -----------------------------------------------------
 */

/*
 * 确定我们希望开始取消冲突事务的截止时间。如果我们愿意永远等待，则返回零（一个安全在过去的时间）。
 */
static TimestampTz fc_GetStandbyLimitTime(void)
{
	TimestampTz fc_rtime;
	bool		fc_fromStream;

	/*
	 * 截止时间是最后一次WAL数据接收时间加上适当的延迟变量。延迟为-1表示永远等待。
	 */
	GetXLogReceiptTime(&fc_rtime, &fc_fromStream);
	if (fc_fromStream)
	{
		if (max_standby_streaming_delay < 0)
			return 0;			/* 永远等待 */
		return TimestampTzPlusMilliseconds(fc_rtime, max_standby_streaming_delay);
	}
	else
	{
		if (max_standby_archive_delay < 0)
			return 0;			/* 永远等待 */
		return TimestampTzPlusMilliseconds(fc_rtime, max_standby_archive_delay);
	}
}

#define STANDBY_INITIAL_WAIT_US  1000
static int	standbyWait_us = STANDBY_INITIAL_WAIT_US;

/*
 * ResolveRecoveryConflictWithVirtualXIDs的备用等待逻辑。
 * 我们在这里等待一段时间，然后返回。如果我们决定不能再等待，则返回真，如果我们可以再等待一段时间，则返回假。
 */
static bool fc_WaitExceedsMaxStandbyDelay(uint32 fc_wait_event_info)
{
	TimestampTz fc_ltime;

	CHECK_FOR_INTERRUPTS();

	/* 我们是否超过了限制时间？ */
	fc_ltime = fc_GetStandbyLimitTime();
	if (fc_ltime && GetCurrentTimestamp() >= fc_ltime)
		return true;

	/*
	 * 稍微休眠一下（这是避免忙等待的必要措施）。
	 */
	pgstat_report_wait_start(fc_wait_event_info);
	pg_usleep(standbyWait_us);
	pgstat_report_wait_end();

	/*
	 * 逐渐增加休眠时间，但不超过1秒，因为
	 * pg_usleep在某些平台上不是可中断的。
	 */
	standbyWait_us *= 2;
	if (standbyWait_us > 1000000)
		standbyWait_us = 1000000;

	return false;
}

/*
 * 记录恢复冲突。
 *
 * wait_start是调用者开始等待的时间戳。
 * now是调用此函数的时间戳。
 * wait_list是分配给
 * 冲突进程的虚拟事务ID列表。still_waiting指示
 * 启动进程是否仍在等待恢复冲突
 * 被解决。
 */
void LogRecoveryConflict(ProcSignalReason fc_reason, TimestampTz fc_wait_start,
					TimestampTz fc_now, VirtualTransactionId *fc_wait_list,
					bool fc_still_waiting)
{
	long		fc_secs;
	int			fc_usecs;
	long		fc_msecs;
	StringInfoData fc_buf;
	int			fc_nprocs = 0;

	/*
	 * 当恢复冲突已经解决时，必须没有冲突进程。
	 */
	Assert(fc_still_waiting || fc_wait_list == NULL);

	TimestampDifference(fc_wait_start, fc_now, &fc_secs, &fc_usecs);
	fc_msecs = fc_secs * 1000 + fc_usecs / 1000;
	fc_usecs = fc_usecs % 1000;

	if (fc_wait_list)
	{
		VirtualTransactionId *fc_vxids;

		/* 构造冲突进程的列表字符串 */
		fc_vxids = fc_wait_list;
		while (VirtualTransactionIdIsValid(*fc_vxids))
		{
			PGPROC	   *fc_proc = BackendIdGetProc(fc_vxids->backendId);

			/* 如果目标后端未激活，proc可以为NULL */
			if (fc_proc)
			{
				if (fc_nprocs == 0)
				{
					initStringInfo(&fc_buf);
					appendStringInfo(&fc_buf, "%d", fc_proc->pid);
				}
				else
					appendStringInfo(&fc_buf, ", %d", fc_proc->pid);

				fc_nprocs++;
			}

			fc_vxids++;
		}
	}

	/*
	 * 如果指定了wait_list，则在详细消息中报告
	 * 活动冲突后端的PID列表。请注意，如果列表中的所有后端
	 * 都不处于活动状态，则不会记录详细消息。
	 */
	if (fc_still_waiting)
	{
		ereport(LOG,
				errmsg("recovery still waiting after %ld.%03d ms: %s",
					   fc_msecs, fc_usecs, get_recovery_conflict_desc(fc_reason)),
				fc_nprocs > 0 ? errdetail_log_plural("Conflicting process: %s.",
												  "Conflicting processes: %s.",
												  fc_nprocs, fc_buf.data) : 0);
	}
	else
	{
		ereport(LOG,
				errmsg("recovery finished waiting after %ld.%03d ms: %s",
					   fc_msecs, fc_usecs, get_recovery_conflict_desc(fc_reason)));
	}

	if (fc_nprocs > 0)
		pfree(fc_buf.data);
}

/*
 * 这是任何与恢复处理冲突的查询后端的主要执行者。对于它的判断
 * 已经在特定的rmgr中做出。在这里，我们只是对进程发出命令。然后进程
 * 根据指示抛出所需的错误。
 *
 * 如果report_waiting为真，"waiting"将在PS显示中报告，并且等待恢复冲突的情况
 * 如果必要的话在日志中报告。如果
 * 调用者负责报告它们，report_waiting应该为false。
 * 否则，调用者和此函数将意外地报告相同的内容。
 */
static void fc_ResolveRecoveryConflictWithVirtualXIDs(VirtualTransactionId *fc_waitlist,
									   ProcSignalReason fc_reason, uint32 fc_wait_event_info,
									   bool fc_report_waiting)
{
	TimestampTz fc_waitStart = 0;
	char	   *fc_new_status = NULL;
	bool		fc_logged_recovery_conflict = false;

	/* 快速退出，以避免在没有工作要做时进行内核调用。 */
	if (!VirtualTransactionIdIsValid(*fc_waitlist))
		return;

	/* 设置用于报告的等待开始时间戳 */
	if (fc_report_waiting && (log_recovery_conflict_waits || update_process_title))
		fc_waitStart = GetCurrentTimestamp();

	while (VirtualTransactionIdIsValid(*fc_waitlist))
	{
		/* 为每个我们等待的xact重置standbyWait_us */
		standbyWait_us = STANDBY_INITIAL_WAIT_US;

		/* 等待直到虚拟xid消失 */
		while (!VirtualXactLock(*fc_waitlist, false))
		{
			/* 该杀死它的时间到了吗？ */
			if (fc_WaitExceedsMaxStandbyDelay(fc_wait_event_info))
			{
				pid_t		fc_pid;

				/*
				 * 现在找出要从气球中抛出的对象。
				 */
				Assert(VirtualTransactionIdIsValid(*fc_waitlist));
				fc_pid = CancelVirtualTransaction(*fc_waitlist, fc_reason);

				/*
				 * 等待一小会儿，让它死掉，以便在系统负载过重时避免
				 * 涌入不响应的后端。
				 */
				if (fc_pid != 0)
					pg_usleep(5000L);
			}

			if (fc_waitStart != 0 && (!fc_logged_recovery_conflict || fc_new_status == NULL))
			{
				TimestampTz fc_now = 0;
				bool		fc_maybe_log_conflict;
				bool		fc_maybe_update_title;

				fc_maybe_log_conflict = (log_recovery_conflict_waits && !fc_logged_recovery_conflict);
				fc_maybe_update_title = (update_process_title && fc_new_status == NULL);

				/* 如果尚未报告，获取当前时间戳 */
				if (fc_maybe_log_conflict || fc_maybe_update_title)
					fc_now = GetCurrentTimestamp();

				/*
				 * 如果我们等待超过500毫秒，通过ps报告（这应该是可配置的吗？）
				 */
				if (fc_maybe_update_title &&
					TimestampDifferenceExceeds(fc_waitStart, fc_now, 500))
				{
					const char *fc_old_status;
					int			fc_len;

					fc_old_status = get_ps_display(&fc_len);
					fc_new_status = (char *) palloc(fc_len + 8 + 1);
					memcpy(fc_new_status, fc_old_status, fc_len);
					strcpy(fc_new_status + fc_len, " waiting");
					set_ps_display(fc_new_status);
					fc_new_status[fc_len] = '\0'; /* 截断掉 " waiting" */
				}

				/*
				 * 如果启动进程等待
				 * 超过deadlock_timeout以解决恢复冲突，则发出日志消息。
				 */
				if (fc_maybe_log_conflict &&
					TimestampDifferenceExceeds(fc_waitStart, fc_now, DeadlockTimeout))
				{
					LogRecoveryConflict(fc_reason, fc_waitStart, fc_now, fc_waitlist, true);
					fc_logged_recovery_conflict = true;
				}
			}
		}

		/* 虚拟事务现在已经消失，等待下一个 */
		fc_waitlist++;
	}

	/*
	 * 如果恢复冲突已解决，但启动
	 * 进程等待了超过deadlock_timeout，发出日志消息。
	 */
	if (fc_logged_recovery_conflict)
		LogRecoveryConflict(fc_reason, fc_waitStart, GetCurrentTimestamp(),
							NULL, false);

	/* 如果我们更改了ps显示，则重置它 */
	if (fc_new_status)
	{
		set_ps_display(fc_new_status);
		pfree(fc_new_status);
	}
}

void ResolveRecoveryConflictWithSnapshot(TransactionId fc_latestRemovedXid, RelFileNode fc_node)
{
	VirtualTransactionId *fc_backends;

	/*
	 * 如果我们经过InvalidTransactionId，那么我们什么都不做（没有冲突）。
	 *
	 * 这可能会发生在重播已经应用的WAL记录后
	 * 备用崩溃或重启，或者在重播一个标记为冻结的XLOG_HEAP2_VISIBLE
	 * 记录，该记录标记为已经全可见的页面。 这
	 * 在索引删除期间生成的记录中也很常见
	 * （删除的原始执行可以推断出，必须在
	 * 删除记录本身的重播之前发生恢复冲突，
	 * 该冲突对删除操作是足够的）。
	 */
	if (!TransactionIdIsValid(fc_latestRemovedXid))
		return;

	fc_backends = GetConflictingVirtualXIDs(fc_latestRemovedXid,
										 fc_node.dbNode);

	fc_ResolveRecoveryConflictWithVirtualXIDs(fc_backends,
										   PROCSIG_RECOVERY_CONFLICT_SNAPSHOT,
										   WAIT_EVENT_RECOVERY_CONFLICT_SNAPSHOT,
										   true);
}

/*
 * 别名ResolveRecoveryConflictWithSnapshot，适用于
 * FullTransactionId值
 */
void ResolveRecoveryConflictWithSnapshotFullXid(FullTransactionId fc_latestRemovedFullXid,
										   RelFileNode fc_node)
{
	/*
	 * ResolveRecoveryConflictWithSnapshot操作在32位TransactionIds上，
	 * 所以截断日志中的FullTransactionId。如果记录的值非常
	 * 旧，以至于XID回绕已经发生在它上面，则不可能有任何
	 * 快照仍然看到它。
	 */
	FullTransactionId fc_nextXid = ReadNextFullTransactionId();
	uint64		fc_diff;

	fc_diff = U64FromFullTransactionId(fc_nextXid) -
		U64FromFullTransactionId(fc_latestRemovedFullXid);
	if (fc_diff < MaxTransactionId / 2)
	{
		TransactionId fc_latestRemovedXid;

		fc_latestRemovedXid = XidFromFullTransactionId(fc_latestRemovedFullXid);
		ResolveRecoveryConflictWithSnapshot(fc_latestRemovedXid, fc_node);
	}
}

void ResolveRecoveryConflictWithTablespace(Oid fc_tsid)
{
	VirtualTransactionId *fc_temp_file_users;

	/*
	 * 备用用户可能正在使用此表空间存储他们的
	 * 临时文件。我们只关心当前用户，因为
	 * temp_tablespace参数将忽略不再存在的表空间。
	 *
	 * 要求所有人立即取消他们的查询，以确保没有
	 * 临时文件残留，我们可以删除该表空间。彻底清除整个
	 * 站点，这是唯一能确保的方式。
	 *
	 * XXX：我们可以通过检查目录中的临时文件名弄清楚
	 * 使用此表空间的活动后端的pids。然后我们将
	 * pid转换为VirtualXIDs，尝试取消
	 * 它们。
	 *
	 * 我们不等待提交，因为删除表空间是非事务性的。
	 */
	fc_temp_file_users = GetConflictingVirtualXIDs(InvalidTransactionId,
												InvalidOid);
	fc_ResolveRecoveryConflictWithVirtualXIDs(fc_temp_file_users,
										   PROCSIG_RECOVERY_CONFLICT_TABLESPACE,
										   WAIT_EVENT_RECOVERY_CONFLICT_TABLESPACE,
										   true);
}

void ResolveRecoveryConflictWithDatabase(Oid fc_dbid)
{
	
/*
	 * 我们不在这里调用 ResolveRecoveryConflictWithVirtualXIDs()，因为这
	 * 只是等待事务，而完全空闲的会话会阻塞我们。这个情况足够罕见，因此
	 * 我们尽可能简单地处理：不等待，立即强制将它们关闭。
	 *
	 * 这里不需要加锁，因为我们已经获取了 AccessExclusiveLock。任何试图
	 * 在我们执行此操作时连接的用户都会在 InitPostgres() 期间阻塞，然后
	 * 在看到数据库已被移除时断开连接。
	 */
	while (CountDBBackends(fc_dbid) > 0)
	{
		CancelDBBackends(fc_dbid, PROCSIG_RECOVERY_CONFLICT_DATABASE, true);

		/*
		 * 等待一段时间让它们死去，以避开在系统负荷重时
		 * 泛滥无响应的后端。
		 */
		pg_usleep(10000);
	}
}

/*
 * ResolveRecoveryConflictWithLock 是从 ProcSleep() 调用的
 * 用以解决与持有关系锁的其他后端之间的冲突。
 *
 * 在 ProcSleep() 中通常进行的 WaitLatch 睡眠（当未处于 InHotStandby
 * 状态时）在这里执行，以增加代码的清晰度。
 *
 * 我们要么立即解决冲突，要么设置一个超时以在我们的耐心极限时唤醒我们。
 *
 * 通过取消所有持有冲突锁的后端来解决冲突。由于我们已经排队等待授予
 * 锁，因此在此期间不会授予任何与我们的锁冲突的新锁请求。
 *
 * 我们还必须检查与启动进程和热备份后端进程有关的死锁。如果在此函数中
 * 达到 deadlock_timeout，则请求所有持有冲突锁的后端检查自身是否发生
 * 死锁。
 *
 * 如果恢复冲突尚未被记录，即使记录功能已启用，logging_conflict 应为
 * true。在达到 deadlock_timeout 并发送死锁检查请求后，如果 logging_conflict
 * 为 false，我们再次等待以通过锁的释放进行信号通知。否则我们返回
 * 而不再等待，以便调用者可以报告恢复冲突。在这种情况下，此函数将再次
 * 被调用，logging_conflict=false（因为恢复冲突已被记录），我们将再次
 * 等待锁的释放。
 */
void ResolveRecoveryConflictWithLock(LOCKTAG fc_locktag, bool fc_logging_conflict)
{
	TimestampTz fc_ltime;
	TimestampTz fc_now;

	Assert(InHotStandby);

	fc_ltime = fc_GetStandbyLimitTime();
	fc_now = GetCurrentTimestamp();

	/*
	 * 如果是在启动进程开始等待锁后第一次循环，则更新 waitStart。
	 * 它不应在每次调用 ResolveRecoveryConflictWithLock() 时更新，
	 * 在等待期间。
	 *
	 * 使用获取的当前时间与 ltime 进行比较作为 waitStart
	 * （即，此进程开始等待锁的时间）。由于重新获取当前时间可能
	 * 导致开销，因此我们重用已获得的时间以避免该开销。
	 *
	 * 请注意，waitStart 在不持有锁表的分区锁的情况下更新，以避免额外
	 * 的锁获取所带来的开销。这会导致在等待开始后的很短时间内，即使
	 * "granted" 为 false，pg_locks 中的 "waitstart" 也会变为 NULL。
	 * 这在实践中是可以的，因为我们可以假设用户遇到长时间等待锁的情况
	 * 时可能更关注 "waitstart"。
	 */
	if (pg_atomic_read_u64(&MyProc->waitStart) == 0)
		pg_atomic_write_u64(&MyProc->waitStart, fc_now);

	if (fc_now >= fc_ltime && fc_ltime != 0)
	{
		/*
		 * 我们已经落后了，所以要尽快清理出一条路径。
		 */
		VirtualTransactionId *fc_backends;

		fc_backends = GetLockConflicts(&fc_locktag, AccessExclusiveLock, NULL);

		/*
		 * 防止 ResolveRecoveryConflictWithVirtualXIDs() 在 PS 显示中报告
		 * "waiting"，通过禁用其参数 report_waiting，因为调用者 WaitOnLock() 
		 * 已经报告了这一点。
		 */
		fc_ResolveRecoveryConflictWithVirtualXIDs(fc_backends,
											   PROCSIG_RECOVERY_CONFLICT_LOCK,
											   PG_WAIT_LOCK | fc_locktag.locktag_type,
											   false);
	}
	else
	{
		/*
		 * 等待（或再次等待）直到 ltime，并检查死锁，如果我们将等待的时间
		 * 超过 deadlock_timeout。
		 */
		EnableTimeoutParams fc_timeouts[2];
		int			fc_cnt = 0;

		if (fc_ltime != 0)
		{
			got_standby_lock_timeout = false;
			fc_timeouts[fc_cnt].id = STANDBY_LOCK_TIMEOUT;
			fc_timeouts[fc_cnt].type = TMPARAM_AT;
			fc_timeouts[fc_cnt].fin_time = fc_ltime;
			fc_cnt++;
		}

		got_standby_deadlock_timeout = false;
		fc_timeouts[fc_cnt].id = STANDBY_DEADLOCK_TIMEOUT;
		fc_timeouts[fc_cnt].type = TMPARAM_AFTER;
		fc_timeouts[fc_cnt].delay_ms = DeadlockTimeout;
		fc_cnt++;

		enable_timeouts(fc_timeouts, fc_cnt);
	}

	/* 等待通过释放 Relation Lock 的信号 */
	ProcWaitForSignal(PG_WAIT_LOCK | fc_locktag.locktag_type);

	/*
	 * 如果 ltime 达到则退出。然后所有持有冲突锁的后端将在下一次
	 * ResolveRecoveryConflictWithLock() 调用中被取消。
	 */
	if (got_standby_lock_timeout)
		goto cleanup;

	if (got_standby_deadlock_timeout)
	{
		VirtualTransactionId *fc_backends;

		fc_backends = GetLockConflicts(&fc_locktag, AccessExclusiveLock, NULL);

		/* 如果没有工作要做则快速退出 */
		if (!VirtualTransactionIdIsValid(*fc_backends))
			goto cleanup;

		/*
		 * 向所有持有冲突锁的后端发送信号，请求他们检查自己是否
		 * 存在死锁。
		 */
		while (VirtualTransactionIdIsValid(*fc_backends))
		{
			SignalVirtualTransaction(*fc_backends,
									 PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK,
									 false);
			fc_backends++;
		}

		/*
		 * 如果恢复冲突尚未记录，即使日志记录已启用，也要退出，以便
		 * 调用方可以记录。然后再次调用 RecoveryConflictWithLock()，我们将
		 * 再次等待锁被释放。
		 */
		if (fc_logging_conflict)
			goto cleanup;

		/*
		 * 在这里再次等待通过释放 Relation Lock 的信号，以防止随后的
		 * RecoveryConflictWithLock() 导致 deadlock_timeout 并再次发送
		 * 死锁检查请求。否则请求将继续在每个 deadlock_timeout 发送，
		 * 直到关系锁被释放或 ltime 达到。
		 */
		got_standby_deadlock_timeout = false;
		ProcWaitForSignal(PG_WAIT_LOCK | fc_locktag.locktag_type);
	}

cleanup:

	/*
	 * 清除上述建立的任何超时请求。我们假设此处的 Startup 进程
	 * 没有其他未处理的超时请求。如果这种情况不再成立，我们可以单独取消
	 * 超时，但那样会更慢。
	 */
	disable_all_timeouts(false);
	got_standby_lock_timeout = false;
	got_standby_deadlock_timeout = false;
}

/*
 * ResolveRecoveryConflictWithBufferPin 从 LockBufferForCleanup() 被调用，
 * 以解决与其他持有缓冲区引脚的后端之间的冲突。
 *
 * 在 LockBufferForCleanup() 中通常进行的 ProcWaitForSignal() 睡眠
 * （当不在热备用时）在这里执行，以提高代码清晰度。
 *
 * 我们要么立即解决冲突，要么设置一个超时在我们的耐心极限时
 * 唤醒我们。
 *
 * 通过向所有后端发送 PROCSIG 信号来解决冲突，以检查它们是否持有
 * 阻塞 Startup 进程的缓冲区引脚。如果是，这些后端将采取适当的
 * 错误动作，ERROR 或 FATAL。
 *
 * 我们还必须检查死锁。死锁发生是因为如果查询在等待一个锁，
 * 那必须是一个 AccessExclusiveLock，在 Startup 进程重播一个事务
 * 完成记录之前，只有在此锁被清除时才能解除。如果 Startup 进程
 * 也在等待，那么就是死锁。死锁可能发生如果查询在等待后，Startup
 * 睡眠，或者如果 Startup 睡眠而查询在等待一个锁。我们在这里
 * 只保护前一种顺序，后者顺序在查询睡眠之前在 CheckRecoveryConflictDeadlock() 
 * 中检查。
 *
 * 死锁极其罕见，检查死锁相对昂贵，因此我们不会立即进行死锁检查......
 * 只有在我们不得不等待至少 deadlock_timeout 时才会进行。
 */
void ResolveRecoveryConflictWithBufferPin(void)
{
	TimestampTz fc_ltime;

	Assert(InHotStandby);

	fc_ltime = fc_GetStandbyLimitTime();

	if (GetCurrentTimestamp() >= fc_ltime && fc_ltime != 0)
	{
		/*
		 * 我们已经落后了，所以要尽快清理出一条路径。
		 */
		fc_SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
	}
	else
	{
		/*
		 * 在 ltime 时醒来，如果我们将等待的时间超过 deadlock_timeout
		 * 也检查死锁。
		 */
		EnableTimeoutParams fc_timeouts[2];
		int			fc_cnt = 0;

		if (fc_ltime != 0)
		{
			fc_timeouts[fc_cnt].id = STANDBY_TIMEOUT;
			fc_timeouts[fc_cnt].type = TMPARAM_AT;
			fc_timeouts[fc_cnt].fin_time = fc_ltime;
			fc_cnt++;
		}

		got_standby_deadlock_timeout = false;
		fc_timeouts[fc_cnt].id = STANDBY_DEADLOCK_TIMEOUT;
		fc_timeouts[fc_cnt].type = TMPARAM_AFTER;
		fc_timeouts[fc_cnt].delay_ms = DeadlockTimeout;
		fc_cnt++;

		enable_timeouts(fc_timeouts, fc_cnt);
	}

	/*
	 * 等待由 UnpinBuffer() 发出的信号，或者等待被上述设定的
	 * 超时中断。
	 *
	 * 我们假设只有 UnpinBuffer() 和上述设定的超时请求可以在这里
	 * 唤醒我们。由 walreceiver 或 SIGHUP 信号处理程序等调用的
	 * WakeupRecovery() 不能做到这一点，因为它使用了与 ProcWaitForSignal()
	 * 等待的不同的闩锁。
	 */
	ProcWaitForSignal(PG_WAIT_BUFFER_PIN);

	if (got_standby_delay_timeout)
		fc_SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_BUFFERPIN);
	else if (got_standby_deadlock_timeout)
	{
		
/*
		 * 发送请求给热备份后端检查自身是否存在死锁。
		 *
		 * XXX 随后的 ResolveRecoveryConflictWithBufferPin() 将会等待
		 * 再次被 UnpinBuffer() 信号指示，如果发生 deadlock_timeout，
		 * 将再次发送死锁检查请求。这导致请求在每次 deadlock_timeout
		 * 发生时持续发送，直到缓冲区被解锁或达到 ltime。这会增加
		 * 启动过程和后端的工作负载。在实践中，这可能并不那么有害，
		 * 因为缓冲区被锁定的时间基本上不会太长。但我们是否应该修复这个问题？
		 */
		fc_SendRecoveryConflictWithBufferPin(PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);
	}

	/*
	 * 清除以上建立的任何超时请求。我们在这里假设
	 * 启动进程没有其他超时，除了此功能使用的超时。
	 * 如果情况不再如此，我们可以单独取消超时，
	 * 但那样会更慢。
	 */
	disable_all_timeouts(false);
	got_standby_delay_timeout = false;
	got_standby_deadlock_timeout = false;
}

static void fc_SendRecoveryConflictWithBufferPin(ProcSignalReason fc_reason)
{
	Assert(fc_reason == PROCSIG_RECOVERY_CONFLICT_BUFFERPIN ||
		   fc_reason == PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK);

	/*
	 * 我们向所有后端发送信号，询问它们是否持有
	 * 延迟启动进程的缓冲区锁。我们暂时不应设置
	 * 冲突标志，因为大多数后端是无辜的。让
	 * 每个后端的 SIGUSR1 处理来决定它们自己的命运。
	 */
	CancelDBBackends(InvalidOid, fc_reason, false);
}

/*
 * 在热备份中执行早期死锁检测。如果在持有
 * 启动进程正在等待的缓冲区锁时即将进入睡眠，
 * 我们将中止锁等待。
 *
 * 注意：这段代码是悲观的，因为它无法确定
 * 是否存在实际的死锁条件：我们需要等待的锁可能
 * 与启动进程持有的锁无关。迟早，这个机制应该被替换，
 * 以某种方式在 DeadLockCheck() 中考虑缓冲区锁。
 * 然而，这里的错误在实践中似乎概率非常低，
 * 所以现在不值得为此烦恼。
 */
void CheckRecoveryConflictDeadlock(void)
{
	Assert(!InRecovery);		/* 在启动进程中不要调用 */

	if (!HoldingBufferPinThatDelaysRecovery())
		return;

	/*
	 * 错误消息应该与 ProcessInterrupts() 匹配，但我们避免在
	 * 这一点上调用它，因为我们并未处理中断。请注意，
	 * 我们在这里只取消当前事务，因此如果我们在一个
	 * 子事务中，而锁被父级持有，则启动进程将继续
	 * 等待，尽管我们已经避免了死锁。
	 */
	ereport(ERROR,
			(errcode(ERRCODE_T_R_DEADLOCK_DETECTED),
			 errmsg("canceling statement due to conflict with recovery"),
			 errdetail("User transaction caused buffer deadlock with recovery.")));
}


/* --------------------------------
 *		超时处理程序例程
 * --------------------------------
 */

/*
 * 如果超过 STANDBY_DEADLOCK_TIMEOUT，将调用 StandbyDeadLockHandler()。
 */
void StandbyDeadLockHandler(void)
{
	got_standby_deadlock_timeout = true;
}

/*
 * 如果超过 STANDBY_TIMEOUT，将调用 StandbyTimeoutHandler()。
 */
void StandbyTimeoutHandler(void)
{
	got_standby_delay_timeout = true;
}

/*
 * 如果超过 STANDBY_LOCK_TIMEOUT，将调用 StandbyLockTimeoutHandler()。
 */
void StandbyLockTimeoutHandler(void)
{
	got_standby_lock_timeout = true;
}

/*
 * -----------------------------------------------------
 * 恢复模式下的锁定
 * -----------------------------------------------------
 *
 * 所有锁由启动进程持有，使用一个虚拟事务。这个实现更简单，
 * 在某种意义上，更正确。持有的锁意味着“某个原始事务持有
 * 这个锁，因此此时不允许查询访问”。因此，启动
 * 进程是实现原始锁的代理。
 *
 * 我们只跟踪 AccessExclusiveLocks，这些锁只由
 * 一个事务在一个关系上持有。
 *
 * 我们在本地内存中维护一个以 xid 为键的锁列表哈希表，
 * RecoveryLockLists，以便跟踪启动进程的虚拟 xid
 * 在共享锁表中做的各种条目。
 *
 * 列表元素使用 xl_standby_lock 类型，因为 WAL 记录类型
 * 正好与我们需要跟踪的信息匹配。
 *
 * 我们使用会话锁而不是普通锁，这样就不需要
 * ResourceOwners。
 */


void StandbyAcquireAccessExclusiveLock(TransactionId fc_xid, Oid fc_dbOid, Oid fc_relOid)
{
	RecoveryLockListsEntry *fc_entry;
	xl_standby_lock *fc_newlock;
	LOCKTAG		fc_locktag;
	bool		fc_found;

	/* 已处理？ */
	if (!TransactionIdIsValid(fc_xid) ||
		TransactionIdDidCommit(fc_xid) ||
		TransactionIdDidAbort(fc_xid))
		return;

	elog(trace_recovery(DEBUG4),
		 "adding recovery lock: db %u rel %u", fc_dbOid, fc_relOid);

	/* 当我们锁定共享关系时，dbOid 为 InvalidOid。 */
	Assert(OidIsValid(fc_relOid));

	/* 为这个 xid 创建一个新的列表，如果我们还没有的话。 */
	fc_entry = hash_search(RecoveryLockLists, &fc_xid, HASH_ENTER, &fc_found);
	if (!fc_found)
	{
		fc_entry->xid = fc_xid;
		fc_entry->locks = NIL;
	}

	fc_newlock = palloc(sizeof(xl_standby_lock));
	fc_newlock->xid = fc_xid;
	fc_newlock->dbOid = fc_dbOid;
	fc_newlock->relOid = fc_relOid;
	fc_entry->locks = lappend(fc_entry->locks, fc_newlock);

	SET_LOCKTAG_RELATION(fc_locktag, fc_newlock->dbOid, fc_newlock->relOid);

	(void) LockAcquire(&fc_locktag, AccessExclusiveLock, true, false);
}

static void fc_StandbyReleaseLockList(List *fc_locks)
{
	ListCell   *fc_lc;

	foreach(fc_lc, fc_locks)
	{
		xl_standby_lock *fc_lock = (xl_standby_lock *) lfirst(fc_lc);
		LOCKTAG		fc_locktag;

		elog(trace_recovery(DEBUG4),
			 "releasing recovery lock: xid %u db %u rel %u",
			 fc_lock->xid, fc_lock->dbOid, fc_lock->relOid);
		SET_LOCKTAG_RELATION(fc_locktag, fc_lock->dbOid, fc_lock->relOid);
		if (!LockRelease(&fc_locktag, AccessExclusiveLock, true))
		{
			elog(LOG,
				 "RecoveryLockLists contains entry for lock no longer recorded by lock manager: xid %u database %u relation %u",
				 fc_lock->xid, fc_lock->dbOid, fc_lock->relOid);
			Assert(false);
		}
	}

	list_free_deep(fc_locks);
}

static void fc_StandbyReleaseLocks(TransactionId fc_xid)
{
	RecoveryLockListsEntry *fc_entry;

	if (TransactionIdIsValid(fc_xid))
	{
		if ((fc_entry = hash_search(RecoveryLockLists, &fc_xid, HASH_FIND, NULL)))
		{
			fc_StandbyReleaseLockList(fc_entry->locks);
			hash_search(RecoveryLockLists, fc_entry, HASH_REMOVE, NULL);
		}
	}
	else
		StandbyReleaseAllLocks();
}

/*
 * 释放事务树的锁，从 xid 向下，来自 RecoveryLockLists。
 *
 * 在热备模式下的 COMMIT/ROLLBACK 的 WAL 重放过程中调用，
 * 以删除事务请求的任何 AccessExclusiveLocks。
 */
void StandbyReleaseLockTree(TransactionId fc_xid, int fc_nsubxids, TransactionId *fc_subxids)
{
	int			fc_i;

	fc_StandbyReleaseLocks(fc_xid);

	for (fc_i = 0; fc_i < fc_nsubxids; fc_i++)
		fc_StandbyReleaseLocks(fc_subxids[fc_i]);
}

/*
 * 在恢复结束时和我们看到关机检查点时调用。
 */
void StandbyReleaseAllLocks(void)
{
	HASH_SEQ_STATUS fc_status;
	RecoveryLockListsEntry *fc_entry;

	elog(trace_recovery(DEBUG2), "release all standby locks");

	hash_seq_init(&fc_status, RecoveryLockLists);
	while ((fc_entry = hash_seq_search(&fc_status)))
	{
		fc_StandbyReleaseLockList(fc_entry->locks);
		hash_search(RecoveryLockLists, fc_entry, HASH_REMOVE, NULL);
	}
}

/*
 * StandbyReleaseOldLocks
 *		释放由未运行的顶层 XIDs 持有的备用锁，
 *		只要它们不是已准备的事务。
 */
void StandbyReleaseOldLocks(TransactionId fc_oldxid)
{
	HASH_SEQ_STATUS fc_status;
	RecoveryLockListsEntry *fc_entry;

	hash_seq_init(&fc_status, RecoveryLockLists);
	while ((fc_entry = hash_seq_search(&fc_status)))
	{
		Assert(TransactionIdIsValid(fc_entry->xid));

		/* 如果是已准备的事务则跳过。 */
		if (StandbyTransactionIdIsPrepared(fc_entry->xid))
			continue;

		/* 如果 >= oldxid 则跳过。 */
		if (!TransactionIdPrecedes(fc_entry->xid, fc_oldxid))
			continue;

		/* 移除所有锁和哈希表条目。 */
		fc_StandbyReleaseLockList(fc_entry->locks);
		hash_search(RecoveryLockLists, fc_entry, HASH_REMOVE, NULL);
	}
}

/*
 * --------------------------------------------------------------------
 *		针对 Rmgr RM_STANDBY_ID 的恢复处理
 *
 * 这些记录类型仅在 XLogStandbyInfoActive() 的情况下创建。
 * --------------------------------------------------------------------
 */

void standby_redo(XLogReaderState *fc_record)
{
	uint8		fc_info = XLogRecGetInfo(fc_record) & ~XLR_INFO_MASK;

	/* 备份块不用于备用记录 */
	Assert(!XLogRecHasAnyBlockRefs(fc_record));

	/* 如果我们不在热备模式下则不执行任何操作 */
	if (standbyState == STANDBY_DISABLED)
		return;

	if (fc_info == XLOG_STANDBY_LOCK)
	{
		xl_standby_locks *fc_xlrec = (xl_standby_locks *) XLogRecGetData(fc_record);
		int			fc_i;

		for (fc_i = 0; fc_i < fc_xlrec->nlocks; fc_i++)
			StandbyAcquireAccessExclusiveLock(fc_xlrec->locks[fc_i].xid,
											  fc_xlrec->locks[fc_i].dbOid,
											  fc_xlrec->locks[fc_i].relOid);
	}
	else if (fc_info == XLOG_RUNNING_XACTS)
	{
		xl_running_xacts *fc_xlrec = (xl_running_xacts *) XLogRecGetData(fc_record);
		RunningTransactionsData fc_running;

		fc_running.xcnt = fc_xlrec->xcnt;
		fc_running.subxcnt = fc_xlrec->subxcnt;
		fc_running.subxid_status = fc_xlrec->subxid_overflow ? SUBXIDS_MISSING : SUBXIDS_IN_ARRAY;
		fc_running.nextXid = fc_xlrec->nextXid;
		fc_running.latestCompletedXid = fc_xlrec->latestCompletedXid;
		fc_running.oldestRunningXid = fc_xlrec->oldestRunningXid;
		fc_running.xids = fc_xlrec->xids;

		ProcArrayApplyRecoveryInfo(&fc_running);
	}
	else if (fc_info == XLOG_INVALIDATIONS)
	{
		xl_invalidations *fc_xlrec = (xl_invalidations *) XLogRecGetData(fc_record);

		ProcessCommittedInvalidationMessages(fc_xlrec->msgs,
											 fc_xlrec->nmsgs,
											 fc_xlrec->relcacheInitFileInval,
											 fc_xlrec->dbId,
											 fc_xlrec->tsId);
	}
	else
		elog(PANIC, "standby_redo: unknown op code %u", fc_info);
}

/*
 * 记录当前快照的详细信息到 WAL。这允许在备用上重建快照状态
 * 并进行逻辑解码。
 *
 * 这用于热备如下：
 *
 * 如果我们从关机检查点启动，我们可以直接转到 STANDBY_SNAPSHOT_READY，
 * 因为我们知道那时没有任何正在运行的内容，我们的恢复快照被知道是空的。
 * 在更典型的在线检查点情况下，我们需要通过一些步骤来获取正确的恢复快照，
 * 这通常需要两个或有时三个阶段的过程。
 *
 * 初始快照必须包含所有正在运行的 xids 和在备用上的所有当前
 * AccessExclusiveLocks。在服务器运行时组装这些信息需要许多不同的
 * LWLocks，所以我们选择逐块推导这些信息，然后在备用上重新组装。
 * 当这些信息完全组装完成时，我们转到 STANDBY_SNAPSHOT_READY。
 *
 * 由于在主服务器上推导信息时锁定不是严格的，我们注意到
 * 推导和写入 WAL 的导出信息之间存在一个时间窗口。
 * 这允许比赛条件，我们必须解决这种情况，因为在该窗口期间
 * xids 和锁可能进入或离开快照。这产生了一个问题，即一个 xid 或
 * 锁可能在快照推导后 *开始* ，但在快照被记录在正在运行的 xacts WAL 记录之前。
 * 我们通过在推导快照之前的一个时间点开始积累更改来解决这个问题，
 * 然后在稍后应用正在运行的 xacts 记录的快照时忽略重复项。
 * 这在 CreateCheckPoint() 中实现，我们使用逻辑检查点位置作为
 * 起始点，然后在写入主检查点 WAL 记录之前立即写入正在运行的 xacts 记录。
 * 由于我们总是从检查点启动，并且立即处于我们的起始点，
 * 我们无条件地转到 STANDBY_INITIALIZED。在这一点之后，
 * 我们必须做四件事：
 *	* 在看到新 xids 时向前移动共享的 nextXid
 *	* 随每个新 xid 扩展 clog 和 subtrans
 *	* 跟踪未提交的已知分配的 xids
 *	* 跟踪未提交的 AccessExclusiveLocks
 *
 * 当我们看到提交/中止时，我们必须从完成的事务中移除已知分配的 xids 和锁。
 * 尝试移除时无法找到条目的情况是可以预期的，并且在我们处于
 * STANDBY_INITIALIZED 状态时不能导致错误。这在 StandbyReleaseLocks() 和
 * KnownAssignedXidsRemove() 中实现。
 *
 * 后来，当我们应用正在运行的 xact 数据时，必须小心忽略
 * 已经提交的事务，因为这些提交在创建 WAL 条目时提前进行了。
 *
 * 松散的时间安排还意味着可能记录锁定时有一个零 xid，因为在移除锁之前
 * xids 会从进程中移除。所以我们必须修剪锁列表，以确保我们仅持有
 * 当前运行的 xids 的锁，这由 StandbyReleaseOldLocks() 执行。
 * 零 xids 应该不再可能，但我们可能正在重放 WAL ，
 * 当时它们是可能的。
 *
 * 对于逻辑解码，仅需要运行的 xacts 信息；
 * 不需要查看锁定信息，但是无论如何都会记录，
 * 因为没有独立的开关仅启用逻辑解码。有关此如何使用的详细信息，请检查
 * snapbuild.c 的介绍注释。
 *
 *
 * 返回最后插入记录的 RecPtr。
 */
XLogRecPtr LogStandbySnapshot(void)
{
	XLogRecPtr	fc_recptr;
	RunningTransactions fc_running;
	xl_standby_lock *fc_locks;
	int			fc_nlocks;

	Assert(XLogStandbyInfoActive());

	/*
	 * 获取当前持有的所有 AccessExclusiveLocks 的详细信息。
	 */
	fc_locks = GetRunningTransactionLocks(&fc_nlocks);
	if (fc_nlocks > 0)
		fc_LogAccessExclusiveLocks(fc_nlocks, fc_locks);
	pfree(fc_locks);

	/*
	 * 记录所有进行中的事务的详细信息。 这应该是我们写入的最后一条
	 * 记录，因为备用将会在看到这个时打开。
	 */
	fc_running = GetRunningTransactionData();

	
/*
	 * GetRunningTransactionData() 获取了 ProcArrayLock，我们必须释放它。
	 * 对于热备用，这可以在插入 WAL 记录之前完成，
	 * 因为 ProcArrayApplyRecoveryInfo() 使用 clog 重新检查提交状态。
	 * 然而，对于逻辑解码，锁不能过早释放，
	 * 因为 clog 从历史快照的角度可能是“未来”的。这将导致我们等待
	 * 在 xl_running_xacts 记录中列出的一个事务结束，
	 * 该事务根据 WAL 显示在 xl_running_xacts 记录之前已提交。
	 * 幸运的是，这个例程的执行频率不高，并且它只是一个共享锁。
	 */
	if (wal_level < WAL_LEVEL_LOGICAL)
		LWLockRelease(ProcArrayLock);

	fc_recptr = fc_LogCurrentRunningXacts(fc_running);

	/* 如果我们保留锁的时间更长，则释放锁... */
	if (wal_level >= WAL_LEVEL_LOGICAL)
		LWLockRelease(ProcArrayLock);

	/* GetRunningTransactionData() 获取了 XidGenLock，我们必须释放它 */
	LWLockRelease(XidGenLock);

	return fc_recptr;
}

/*
 * 将正在运行的事务的增强快照记录到 WAL 中。
 *
 * RunningTransactionsData 和 xl_running_xacts 的定义是相似的。我们保持它们分开，
 * 因为 xl_running_xacts 是一块连续的内存块，并且在组装到 WAL 中之前永远不会完全存在。
 * 插入的记录被标记为不重要以保证持久性，以避免触发多余的检查点/归档活动。
 */
static XLogRecPtr fc_LogCurrentRunningXacts(RunningTransactions fc_CurrRunningXacts)
{
	xl_running_xacts fc_xlrec;
	XLogRecPtr	fc_recptr;

	fc_xlrec.xcnt = fc_CurrRunningXacts->xcnt;
	fc_xlrec.subxcnt = fc_CurrRunningXacts->subxcnt;
	fc_xlrec.subxid_overflow = (fc_CurrRunningXacts->subxid_status != SUBXIDS_IN_ARRAY);
	fc_xlrec.nextXid = fc_CurrRunningXacts->nextXid;
	fc_xlrec.oldestRunningXid = fc_CurrRunningXacts->oldestRunningXid;
	fc_xlrec.latestCompletedXid = fc_CurrRunningXacts->latestCompletedXid;

	/* Header */
	XLogBeginInsert();
	XLogSetRecordFlags(XLOG_MARK_UNIMPORTANT);
	XLogRegisterData((char *) (&fc_xlrec), MinSizeOfXactRunningXacts);

	/* 事务ID 数组 */
	if (fc_xlrec.xcnt > 0)
		XLogRegisterData((char *) fc_CurrRunningXacts->xids,
						 (fc_xlrec.xcnt + fc_xlrec.subxcnt) * sizeof(TransactionId));

	fc_recptr = XLogInsert(RM_STANDBY_ID, XLOG_RUNNING_XACTS);

	if (fc_xlrec.subxid_overflow)
		elog(trace_recovery(DEBUG2),
			 "snapshot of %u running transactions overflowed (lsn %X/%X oldest xid %u latest complete %u next xid %u)",
			 fc_CurrRunningXacts->xcnt,
			 LSN_FORMAT_ARGS(fc_recptr),
			 fc_CurrRunningXacts->oldestRunningXid,
			 fc_CurrRunningXacts->latestCompletedXid,
			 fc_CurrRunningXacts->nextXid);
	else
		elog(trace_recovery(DEBUG2),
			 "snapshot of %u+%u running transaction ids (lsn %X/%X oldest xid %u latest complete %u next xid %u)",
			 fc_CurrRunningXacts->xcnt, fc_CurrRunningXacts->subxcnt,
			 LSN_FORMAT_ARGS(fc_recptr),
			 fc_CurrRunningXacts->oldestRunningXid,
			 fc_CurrRunningXacts->latestCompletedXid,
			 fc_CurrRunningXacts->nextXid);

	/*
	 * 确保 running_xacts 信息在不太遥远的未来同步到磁盘。我们不想阻止任何事情（即使用 XLogFlush()），
	 * 所以我们让 WAL 写入器在正常操作期间完成这一任务。
	 * XLogSetAsyncXactLSN() 方便地将 LSN 标记为待同步，
	 * 并在休眠时推动 WALWriter 进行操作。有关某条记录在没有它的情况下可能不会被冲刷的详细信息，
	 * 请参见 XLogBackgroundFlush()。
	 */
	XLogSetAsyncXactLSN(fc_recptr);

	return fc_recptr;
}

/*
 * 大规模记录 AccessExclusiveLocks。其他锁类型不需要记录，
 * 具体说明见 backend/storage/lmgr/README。
 */
static void fc_LogAccessExclusiveLocks(int fc_nlocks, xl_standby_lock *fc_locks)
{
	xl_standby_locks fc_xlrec;

	fc_xlrec.nlocks = fc_nlocks;

	XLogBeginInsert();
	XLogRegisterData((char *) &fc_xlrec, offsetof(xl_standby_locks, locks));
	XLogRegisterData((char *) fc_locks, fc_nlocks * sizeof(xl_standby_lock));
	XLogSetRecordFlags(XLOG_MARK_UNIMPORTANT);

	(void) XLogInsert(RM_STANDBY_ID, XLOG_STANDBY_LOCK);
}

/*
 * 单独记录 AccessExclusiveLocks 以便在 LockAcquire() 中使用
 */
void LogAccessExclusiveLock(Oid fc_dbOid, Oid fc_relOid)
{
	xl_standby_lock fc_xlrec;

	fc_xlrec.xid = GetCurrentTransactionId();

	fc_xlrec.dbOid = fc_dbOid;
	fc_xlrec.relOid = fc_relOid;

	fc_LogAccessExclusiveLocks(1, &fc_xlrec);
	MyXactFlags |= XACT_FLAGS_ACQUIREDACCESSEXCLUSIVELOCK;
}

/*
 * 准备记录一个 AccessExclusiveLock，以便在 LockAcquire() 中使用
 */
void LogAccessExclusiveLockPrepare(void)
{
	/*
	 * 确保该事务已分配一个 TransactionId，出于两个原因，
	 * 两个原因均与备用上的锁释放有关。首先，我们必须分配一个 xid，
	 * 以便 RecordTransactionCommit() 和 RecordTransactionAbort() 不优化掉
	 * 恢复所依赖的事务完成记录，从而释放锁。这是一种临时解决方案，
	 * 但在主提交路径中添加代码对于边缘情况来说不值得。其次，我们必须在锁记录在
	 * 共享内存中之前分配一个 xid，否则一个并发执行的
	 * GetRunningTransactionLocks() 可能会看到一个与 InvalidTransactionId 相关的锁，
	 * 而我们后续断言这不可发生。
	 */
	(void) GetCurrentTransactionId();
}

/*
 * 发出无效化的 WAL。目前这仅用于没有 xid 的提交，但包含无效化。
 */
void LogStandbyInvalidations(int fc_nmsgs, SharedInvalidationMessage *fc_msgs,
						bool fc_relcacheInitFileInval)
{
	xl_invalidations fc_xlrec;

	/* 准备记录 */
	memset(&fc_xlrec, 0, sizeof(fc_xlrec));
	fc_xlrec.dbId = MyDatabaseId;
	fc_xlrec.tsId = MyDatabaseTableSpace;
	fc_xlrec.relcacheInitFileInval = fc_relcacheInitFileInval;
	fc_xlrec.nmsgs = fc_nmsgs;

	/* 执行插入 */
	XLogBeginInsert();
	XLogRegisterData((char *) (&fc_xlrec), MinSizeOfInvalidations);
	XLogRegisterData((char *) fc_msgs,
					 fc_nmsgs * sizeof(SharedInvalidationMessage));
	XLogInsert(RM_STANDBY_ID, XLOG_INVALIDATIONS);
}

/* 返回恢复冲突的描述 */
static const char * get_recovery_conflict_desc(ProcSignalReason fc_reason)
{
	const char *fc_reasonDesc = _("unknown reason");

	switch (fc_reason)
	{
		case PROCSIG_RECOVERY_CONFLICT_BUFFERPIN:
			fc_reasonDesc = _("recovery conflict on buffer pin");
			break;
		case PROCSIG_RECOVERY_CONFLICT_LOCK:
			fc_reasonDesc = _("recovery conflict on lock");
			break;
		case PROCSIG_RECOVERY_CONFLICT_TABLESPACE:
			fc_reasonDesc = _("recovery conflict on tablespace");
			break;
		case PROCSIG_RECOVERY_CONFLICT_SNAPSHOT:
			fc_reasonDesc = _("recovery conflict on snapshot");
			break;
		case PROCSIG_RECOVERY_CONFLICT_STARTUP_DEADLOCK:
			fc_reasonDesc = _("recovery conflict on buffer deadlock");
			break;
		case PROCSIG_RECOVERY_CONFLICT_DATABASE:
			fc_reasonDesc = _("recovery conflict on database");
			break;
		default:
			break;
	}

	return fc_reasonDesc;
}
